package com.db.client;

import com.db.client.UIs.ConsoleFactory;
import com.db.client.UIs.Factory;
import com.db.exceptions.CommandLineException;
import com.db.exceptions.ExitException;
import com.db.exceptions.InvalidKeyValueException;
import com.db.protocol.Command;
import com.db.protocol.Request;

import java.io.*;
import java.net.Socket;
import java.net.SocketException;
import java.util.*;

public class ChatClient {

    public static final int DEFAULT_PORT = 1234;
    public static final String DEFAULT_IP = "127.0.0.1";
    public static final int MAX_FAIL_TIMES = 5;
    public static final int MAX_ROOM_NAME = 150;
    public static final int MAX_NICK_NAME = 150;
    public static final int MAX_MSG_SIZE = 150;
    public static final int MAX_PASSWORD = 150;

    public static Factory factory = new ConsoleFactory();

    private ChatPrinter chatPrinter;
    private static UI ui;

    private int port = DEFAULT_PORT;
    private String ip = DEFAULT_IP;

    private ChatClient(UI ui) {
        this.ui = ui;
    }

    public static void main(String... args) {

        ChatClient chat = new ChatClient(factory.getUI());

        Iterator<Parameter> params;
        try {
            params = parseCommands(args).iterator();
            Parameter param;
            while (params.hasNext()) {
                param = params.next();
                switch (param) {
                    case ip:
                        chat.ip = param.value();
                        break;
                    case port:
                        chat.port = Integer.parseInt(param.value());
                        break;
                    default:
                        break;
                }
            }
            chat.startSession();
        } catch (CommandLineException e) {
            ui.error("UIs fail");
        } catch (ExitException e) {
            return;
        } catch (Exception e) {
            ui.error("unpredictable error, shutdown");
            return;
        }
    }

    private void startSession() throws IOException, ExitException {
        while (!Thread.currentThread().isInterrupted()) {
            try (
                    Socket socket = new Socket(ip, port);
                    ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
                    ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
            ) {
                ui.error("connected to " + ip + ":" + port);
                register(out, in);
                chatPrinter = new ChatPrinter(in, ui);
                chatPrinter.start();
                chat(out);
            } catch (SocketException e) {
                ui.error("server down, reconnect");
            } catch (NullPointerException e) {
                ui.error("critical error, shutdown");
                Thread.currentThread().interrupt();
            } finally {
                if (chatPrinter != null) {
                    chatPrinter.interrupt();
                }
            }
            try {
                Thread.currentThread().sleep(2000);
            } catch (InterruptedException e) {}
        }
    }

    private void register(ObjectOutputStream out, ObjectInputStream in) throws SocketException {
        Object response = null;
        boolean registered = false;
        do {
            String nickname, password;
            StringTokenizer tokenizer = new StringTokenizer(ui.getLine("register with \"<nickname> <password>\": "));
            if ("" == (nickname = validateNickname(tokenizer))
            ||  "" == (password = validatePassword(tokenizer))) {
                continue;
            }
            try {
                out.writeObject(new Request(Command.chid, new String[]{nickname, password}));
                response = in.readObject();
                if (response instanceof String) {
                    String res = (String)response;
                    if ("nickname accepted".equals(res)) {
                        registered = true;
                    } else {
                        ui.printMessages(res);
                    }
                }
            } catch (ClassNotFoundException | IOException e) {
                ui.error("failed request");
                throw new SocketException();
            }
        } while (!registered);
        ui.printMessages("successfully logged in");
    }

    private void chat(ObjectOutputStream out) throws ExitException {
        String line = ui.getLine();
        while (true) {
            if ("exit".equals(line)) {
                throw new ExitException();
            }
            StringTokenizer tokenizer = new StringTokenizer(line);
            String instruction;
            try {
                instruction = tokenizer.nextToken(" ");
            } catch (NoSuchElementException e) {
                line = ui.getLine();
                continue;
            }
            String[] param = null, p = new String[2];
            Command command = null;
            switch (instruction) {
                case "help":
                    ui.printMessages(">>> snd <message>",
                            ">>> sndp <nickname> <message>",
                            ">>> chid <nickname> <password>",
                            ">>> chroom <room name>",
                            ">>> gtrooms",
                            ">>> hist",
                            ">>> histr",
                            ">>> histp",
                            ">>> help",
                            ">>> exit");
                    break;
                case "gtrooms":
                    command = Command.gtrooms;
                    break;
                case "hist":
                    command = Command.hist;
                    break;
                case "histp":
                    command = Command.histp;
                    break;
                case "histr":
                    command = Command.histr;
                    break;
                case "chid":
                    if ("" != (p[0] = validateNickname(tokenizer))
                    &&  "" != (p[1] = validatePassword(tokenizer))) {
                        param = new String[]{p[0], p[1]};
                        command = Command.chid;
                    }
                    break;
                case "chroom":
                    if ("" != (p[0] = validateRoomname(tokenizer))) {
                        param = new String[]{p[0]};
                        command = Command.chroom;
                    }
                    break;
                case "snd":
                    if ("" != (p[0] = validateMessage(tokenizer))) {
                        param = new String[]{p[0]};
                        command = Command.snd;
                    }
                    break;
                case "sndp":
                    if ("" != (p[0] = validateReceiverNickname(tokenizer))
                    &&  "" != (p[1] = validateMessage(tokenizer))) {
                        param = new String[]{p[0], p[1]};
                        command = Command.sndp;
                    }
                    break;
                default:
                    ui.error("invalid instruction");
                    break;
            }
            try {
                if (command != null) {
                    out.writeObject(new Request(command, param));
                }
            } catch (SocketException e) {
                ui.error("server down");
                return;
            } catch (IOException e) {
                ui.error("failed sending " + command.toString() + " request");
            }
            line = ui.getLine();
        }
    }

    private static String validateNickname(StringTokenizer tokenizer) {
        return validateParam(tokenizer, " ", MAX_NICK_NAME, "nickname", true);
    }

    private static String validateRoomname(StringTokenizer tokenizer) {
        return validateParam(tokenizer, "", MAX_ROOM_NAME, "room name", true);
    }

    private static String validateReceiverNickname(StringTokenizer tokenizer) {
        return validateParam(tokenizer, " ", MAX_NICK_NAME, "receiver name", true);
    }

    private static String validatePassword(StringTokenizer tokenizer) {
        return validateParam(tokenizer, "", MAX_PASSWORD, "password", true);
    }

    private static String validateMessage(StringTokenizer tokenizer) {
        return validateParam(tokenizer, "", MAX_MSG_SIZE, "message", false);
    }

    private static String validateParam(StringTokenizer tokenizer, String separator, int length, String who, boolean spaceCheck) {
        if (!tokenizer.hasMoreTokens()) {
            ui.error("no param");
            return "";
        }
        String param = tokenizer.nextToken(separator).trim();
        if ((param.length() > 150)){
            ui.error(who + " too long (" + length + " max)");
            return "";
        }
        if ((param.length() < 1)){
            ui.error(who + " too short");
            return "";
        }
        if (spaceCheck && param.contains(" ")) {
            ui.error(who + " cant contain spaces");
            return "";
        }
        return param;
    }

    private enum Parameter {
        ip, port;

        private Parameter setValue(String value) {
            this.value = value;
            return this;
        }

        private String value() {
            return value;
        }

        private String value;
    }

    static List<Parameter> parseCommands(String... args) throws CommandLineException {
        List<Parameter> params = new ArrayList<>();
        for (int i = 0; i < args.length; ++i) {
            switch (args[i]) {
                case "-ip":
                    if (++i >= args.length) throw new CommandLineException("-ip key without value");
                    if (!validateIP(args[i])) throw new InvalidKeyValueException("ip", args[i]);
                    params.add(Parameter.ip.setValue(args[i]));
                    break;
                case "-port":
                    if (++i >= args.length) throw new CommandLineException("-port key without value");
                    try { //validating port
                        Integer.parseInt(args[i]);
                        params.add(Parameter.port.setValue(args[i]));
                    } catch (NumberFormatException e) {
                        throw new InvalidKeyValueException("port", args[i]);
                    }
                    break;
                default:
                    throw new CommandLineException("invalid key:" + args[i]);
            }
        }
        return params;
    }

    public static boolean validateIP(String ip) {
        return true;
    }
}
